package graphicsExtended.matrix;

import graphicsExtended.animation.drawer.StillFrameDrawer;
import graphicsExtended.animation.animationSequence.Drawable2;
import graphicsExtended.animation.animationSequence.WrapperStillFrame;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.image.BufferedImage;
import math.mathExtended.MathExtended;
import processor.images.ImageProcessor;

/**
 *
 * @author Shimu
 * @date 12-Jul-2012
 */
public class Table_Drawable<T extends Drawable2> extends Table<T> {

    /**
     * No margins. This is the default setting.
     */
    public static final int MARGIN_NONE = 0;

    /**
     * Margins at the top.
     */
    public static final int MARGIN_TOP = 2;

    /**
     * Margins at the bottom.
     */
    public static final int MARGIN_BOTTOM = 4;

    /**
     * Margins on the left.
     */
    public static final int MARGIN_LEFT = 8;

    /**
     * Margins on the right.
     */
    public static final int MARGIN_RIGHT = 16;

    /**
     * Margins on all sides (left, right, top, bottom)
     */
    public static final int MARGIN_ALL = MARGIN_TOP | MARGIN_BOTTOM
            | MARGIN_LEFT | MARGIN_RIGHT;

    /** An integer representing the type of margin this grid has */
    private final int MARGIN_TYPE;

    // The offsets to the indexes caused by the margins
    private final int TOP_OFFSET, BOTTOM_OFFSET, LEFT_OFFSET, RIGHT_OFFSET;

    /**
     * No border surrounding each cell
     */
    public static final int CELL_BORDER_NONE = 0;

    /**
     * Borders on the bottom of each cell
     */
    public static final int CELL_BORDER_BOTTOM = 2;

    /**
     * Borders on the top of each cell
     */
    public static final int CELL_BORDER_TOP = 4;

    /**
     * Borders on the right of each cell
     */
    public static final int CELL_BORDER_RIGHT = 8;

    /**
     * Borders on the left of each cell
     */
    public static final int CELL_BORDER_LEFT = 16;

    /**
     * A square border surrounding each cell
     */
    public static final int CELL_BORDER_ALL = CELL_BORDER_BOTTOM
            | CELL_BORDER_TOP | CELL_BORDER_RIGHT | CELL_BORDER_LEFT;

    /** An integer representing the type of border this grid has */
    private final int BORDER_TYPE;

    /**
     * The color of the border
     */
    private final Color BORDER_COLOR = Color.GRAY;

    /**
     * The color of the border when a cell is selected
     */
    private final Color BORDER_COLOR_SELECTED = Color.BLUE;

    // The width and height of each grid
    private final int CELL_WIDTH, CELL_HEIGHT;

    private final int TABLE_WIDTH, TABLE_HEIGHT;

    private final int BORDER_THICKNESS;

    /**
     * A BufferedImage with the borders drawn around it, the center is 
     * transparent. One can easily draw this BufferedImage onto another
     * BufferedImage to make the image have the same borders.
     */
    private final BufferedImage BORDER;

    /** 
     * A BufferedImage with the borders drawn around it, the center is 
     * transparent. One can easily draw this BufferedImage onto another
     * BufferedImage to make the image have the same borders.   
     */
    private final BufferedImage BORDER_SELECTED;

    /**
     * A BufferedImage with a white background and borders drawn around it
     */
    private final Image EMPTY_CELL;

    /**
     * The StillFrameDrawer for this Table_Drawable.
     */
    private final StillFrameDrawer DRAWER;

    /**
     * 
     * @param TABLE_WIDTH
     * @param TABLE_HEIGHT
     * @param getTotalColumns()
     * @param maxY
     * @param MARGIN_TYPE an integer that is a bitwise OR (|) of one or more of 
     * MARGIN_NONE, MARGIN_RIGHT, MARGIN_LEFT, MARGIN_TOP, MARGIN_BOTTOM. 
     */
    public Table_Drawable(int screenWidth, int screenHeight,
            int maxX, int maxY, StillFrameDrawer drawer,
            BufferedImage emptyCellImg) {
        this(screenWidth, screenHeight, maxX, maxY,
                MARGIN_NONE, CELL_BORDER_NONE, drawer, emptyCellImg);
    }

    /**
     * @param width the width of the table
     * @param height the height of the table
     * @param maxX the total number of elements in a column
     * @param maxY the total number of elements in a row
     * @param MARGIN_TYPE an integer that is a bitwise OR (|) of one or more of 
     * MARGIN_NONE, MARGIN_RIGHT, MARGIN_LEFT, MARGIN_TOP, MARGIN_BOTTOM. 
     * @throws TableException if the width or height is too small for the
     * number of elements in a column or row respectively
     */
    public Table_Drawable(int width, int height, int maxX, int maxY,
            int marginType, int borderType, StillFrameDrawer drawer,
            BufferedImage emptyCellImg) {

        // Make superclass allocate enough room for the margin as well
        super(maxX + (((marginType & MARGIN_LEFT) > 0) ? 1 : 0)
                + (((marginType & MARGIN_RIGHT) > 0) ? 1 : 0),
                maxY + (((marginType & MARGIN_TOP) > 0) ? 1 : 0)
                + (((marginType & MARGIN_BOTTOM) > 0) ? 1 : 0));

        this.MARGIN_TYPE = marginType;

        // Offsets needed to index into the matrix
        this.TOP_OFFSET = hasTopMargin() ? 1 : 0;
        this.BOTTOM_OFFSET = hasBottomMargin() ? 1 : 0;
        this.LEFT_OFFSET = hasLeftMargin() ? 1 : 0;
        this.RIGHT_OFFSET = hasRightMargin() ? 1 : 0;

        this.BORDER_TYPE = borderType;

        // Calculate the grid width based on maxX, LEFT_OFFSET, 
        // and RIGHT_OFFSET
        this.CELL_WIDTH = (int) (width / (float) super.getTotalColumns());
        if (this.CELL_WIDTH == 0) {
            throw new TableException(
                    "Width of the table too small for the total number of elements in a column");
        }

        // Calculate the grid height based on maxY, TOP_OFFSET, 
        // and BOTTOM_OFFSET
        this.CELL_HEIGHT = (int) (height / (float) super.getTotalRows());
        if (this.CELL_HEIGHT == 0) {
            throw new TableException(
                    "Height of the table too small for the total number of elements in a row");
        }

        /*
        System.out.printf("Cell width, height: (%d, %d)\n", 
        this.CELL_WIDTH, this.CELL_HEIGHT);
         */

        this.BORDER_THICKNESS = (int) (0.07 * Math.min(CELL_WIDTH, CELL_HEIGHT));

        this.TABLE_WIDTH = width;
        this.TABLE_HEIGHT = height;

        this.DRAWER = drawer;

        this.BORDER = this.createBorder(BORDER_COLOR, CELL_WIDTH, CELL_HEIGHT,
                BORDER_THICKNESS);

        this.BORDER_SELECTED = this.createBorder(BORDER_COLOR_SELECTED,
                CELL_WIDTH, CELL_HEIGHT, BORDER_THICKNESS);

        this.EMPTY_CELL = (emptyCellImg == null) ? null : scaleIcon(emptyCellImg);

        drawAll();
    }

    public void selected(int x, int y) {
        draw(x, y, BORDER_SELECTED);
    }

    public void deselected(int x, int y) {
        draw(x, y);
    }

    /**
     * Returns true if the cell at the given (x, y) coordinate is empty.
     * Coordinate is relative.
     * @param x
     * @param y
     * @return 
     */
    @Override
    public boolean isEmpty(int x, int y) {
        return super.isEmpty(convertX(x), convertY(y));
    }

    /**
     * Draw a border that will surround the given Graphics2D with
     * CELL_WIDTH and CELL_HEIGHT
     * 
     * @param g 
     */
    private BufferedImage createBorder(Color borderColor,
            int cellWidth, int cellHeight, int borderThickness) {
        BufferedImage border = new BufferedImage(CELL_WIDTH, CELL_HEIGHT,
                BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = border.createGraphics();
        g.setStroke(new BasicStroke(borderThickness));
        g.setColor(borderColor);
        if (hasTopBorder()) {
            g.drawLine(0, 0, cellWidth, 0);
        }
        if (hasBottomBorder()) {
            g.drawLine(0, cellHeight, cellWidth, cellHeight);
        }
        if (hasLeftBorder()) {
            g.drawLine(0, 0, 0, cellHeight);
        }
        if (hasRightBorder()) {
            g.drawLine(cellWidth, 0, cellWidth, cellHeight);
        }
        return border;
    }

    /**
     * Converts absolute table coordinate to screen coordinate
     * @param x
     * @param y
     * @return 
     */
    public Point tableToScreen(int x, int y) {
        return new Point(x * this.CELL_WIDTH, y * this.CELL_HEIGHT);
    }

    /**
     * Adds the given T newObj to the cell at (x, y). Each time an object is 
     * set into the table, its icon will be checked for size and scaled if 
     * necessary. Coordinate is relative.
     * 
     * @param x
     * @param y
     * @param newObj
     * @return 
     */
    @Override
    public T set(int x, int y, T newObj) {
        int convertedX = convertX(x);
        int convertedY = convertY(y);

        T returnVal = super.set(convertedX, convertedY, newObj);
        Image icon = newObj.getIcon();
        if (icon != null) {
            newObj.setIcon(scaleIcon(icon));
        }
        draw(convertedX, convertedY);
        return returnVal;
    }

    /**
     * Returns the T object at the given (x, y) coordinate, null if no T object
     * was at the given (x, y) coordinate. Coordinate is relative.
     * 
     * @param x
     * @param y
     * @return 
     */
    @Override
    public T get(int x, int y) {
        return super.get(convertX(x), convertY(y));
    }
    
    /**
     * Remove and return the T object at the given (x, y) coordinate. 
     * Returns null if no T object was at the given (x, y) coordinate. 
     * Coordinate is relative.
     * @param x
     * @param y
     * @return 
     */
    @Override
    public T remove(int x, int y) {
        int convertedX = convertX(x);
        int convertedY = convertY(y);
        T returnVal = super.remove(convertedX, convertedY);
        erase(convertedX, convertedY);
        return returnVal;
    }

    /**
     * Remove all elements from this Table
     */
    public void removeAll() {
        for (int x = 0; x < this.getColumns(); x++) {
            for (int y = 0; y < this.getRows(); y++) {
                this.remove(x, y);
            }
        }
    }

    // <editor-fold defaultstate="collapsed" desc="Private coordinate helpers">  
    /**
     * Converts x into a coordinate that takes into account the offsets.
     * @param x
     * @throw IndexOutOfBoundsException if the coordinate is invalid
     * @return 
     */
    private int convertX(int x) {
        x += this.LEFT_OFFSET;
        if (MathExtended.bounded(x, LEFT_OFFSET, this.getColumns())) {
            return x;
        } else {
            throw new IndexOutOfBoundsException();
        }
    }

    /**
     * Converts y into a coordinate that takes into account the offsets.
     * @param y
     * @throw IndexOutOfBoundsException if the coordinate is invalid
     * @return 
     */
    private int convertY(int y) {
        y += this.TOP_OFFSET;
        if (MathExtended.bounded(y, TOP_OFFSET,
                this.getTotalRows() - BOTTOM_OFFSET)) {
            return y;
        } else {
            throw new IndexOutOfBoundsException();
        }
    }
    // </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="Private drawing helpers">  
    private void erase(int x, int y) {
        Point screenCoord = this.tableToScreen(x, y);
        this.DRAWER.erase(screenCoord.x, screenCoord.y, 
                this.CELL_WIDTH, this.CELL_HEIGHT);
    }
    
        /**
     * Push all of the Table_Drawable's cell into the given Drawable,
     * including the margins
     */
    private void drawAll() {
        for (int x = 0; x < this.getTotalColumns(); x++) {
            for (int y = 0; y < this.getTotalRows(); y++) {
                draw(x, y);
            }
        }
    }

    /**
     * Push the image representation of the cell at (x, y) onto the DRAWER.
     * Coordinate is absolute.
     * 
     * @param x
     * @param y 
     */
    private void draw(int x, int y, Image border) {
        final Image icon;
        if (super.get(x, y) == null) {
            icon = null;
        } else {
            icon = super.get(x, y).getIcon();
        }
        
        final Point screenCoordinate = tableToScreen(x, y);
        if (icon != null) {
            this.DRAWER.draw(new WrapperStillFrame(icon, screenCoordinate));
        }
        this.DRAWER.draw(new WrapperStillFrame(border, screenCoordinate));
    }

    /**
     * Coordinate is absolute.
     * @param x
     * @param y
     * @param img 
     */
    private void draw(int x, int y) {
        draw(x, y, BORDER);
    }

    /**
     * Returns a scaled version of the given original.
     * 
     * @param original
     * @return 
     */
    private Image scaleIcon(Image original) {
        Image scaled;
        if ((original.getWidth(null) > this.CELL_WIDTH)
                || (original.getHeight(null) > this.CELL_HEIGHT)) {
            scaled = ImageProcessor.scaleToFit(original,
                    (int) this.CELL_WIDTH, (int) this.CELL_HEIGHT,
                    Image.SCALE_SMOOTH);
            return scaled;
        }
        return original;
    }
    // </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="Getters">  
    /**
     * @return the BORDER_THICKNESS in pixels
     */
    public int getBorderThickness() {
        return BORDER_THICKNESS;
    }

    /**
     * @return the BORDER_COLOR
     */
    public Color getBorderColor() {
        return BORDER_COLOR;
    }

    /**
     * Returns the total number of cells in a row, including the margins.
     * @return 
     */
    @Override
    public int getTotalColumns() {
        return super.getTotalColumns();
    }

    /**
     * Returns the total number of cells in a column, including the margins.
     * @return 
     */
    @Override
    public int getTotalRows() {
        return super.getTotalRows();
    }

    /**
     * Returns the total number of cells, including the margins.
     * @return 
     */
    @Override
    public int getTotalCells() {
        return super.getTotalCells();
    }

    /**
     * Returns the total number of cells in a row, excluding the margins.
     * @return 
     */
    public int getColumns() {
        return getTotalColumns() - this.LEFT_OFFSET - this.RIGHT_OFFSET;
    }

    /**
     * Returns the total number of cells in a column, excluding the margins.
     * @return 
     */
    public int getRows() {
        return getTotalRows() - this.TOP_OFFSET - this.BOTTOM_OFFSET;
    }

    /**
     * Returns the total number of cells, excluding the margins.
     * @return 
     */
    public int getMaxCell() {
        return getColumns() * getRows();
    }

    /**
     * Returns the width of each cell.
     * @return 
     */
    public int getCellWidth() {
        return CELL_WIDTH;
    }

    /**
     * Returns the height of each cell
     * @return 
     */
    public int getCellHeight() {
        return CELL_HEIGHT;
    }

    public int getTableWidth() {
        return TABLE_WIDTH;
    }

    public int getTableHeight() {
        return TABLE_HEIGHT;
    }

    /**
     * Returns an integer representing the margin type of this 
     * InteractiveGrid. AND (&) the returned value with one of 
     * MARGIN_NONE, MARGIN_RIGHT, MARGIN_LEFT, MARGIN_TOP, MARGIN_BOTTOM, to find
     * if this InteractiveGrid contains a particular type of margin. 
     * @return 
     */
    public final int getMarginType() {
        return MARGIN_TYPE;
    }

    /**
     * @return the TOP_OFFSET
     */
    public int getTopOffset() {
        return TOP_OFFSET;
    }

    /**
     * @return the BOTTOM_OFFSET
     */
    public int getBottomOffset() {
        return BOTTOM_OFFSET;
    }

    /**
     * @return the LEFT_OFFSET
     */
    public int getLeftOffset() {
        return LEFT_OFFSET;
    }

    /**
     * @return the RIGHT_OFFSET
     */
    public int getRightOffset() {
        return RIGHT_OFFSET;
    }
    // </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="Setters">  
    /**
     * Sets the top label at the given index.
     * 
     * @param index
     * @param obj
     * @return the T that use to occupy the given index
     * @throws IndexOutOfBoundsException if the given index is outside the
     * bounds of the top labels or if this InteractiveGrid was not
     * instantiated to have top labels.
     * @return 
     */
    public T setTopLabel(int index, T obj) {
        if (this.TOP_OFFSET == 0) {
            throw new UnsupportedOperationException("This grid was not "
                    + "instantiated to have labels on top!");
        }
        return super.set(index, 0, obj);
    }

    /**
     * Sets the bottom label at the given index.
     * 
     * @param index
     * @param obj
     * @throws IndexOutOfBoundsException if the given index is outside the
     * bounds of the bottom labels or if this InteractiveGrid was not
     * instantiated to have bottom labels.
     * @return 
     */
    public T setBottomLabel(int index, T obj) {
        if (this.BOTTOM_OFFSET == 0) {
            throw new UnsupportedOperationException("This grid was not "
                    + "instantiated to have labels on the bottom!");
        }
        return super.set(index, this.getTotalRows() - 1, obj);
    }

    /**
     * Sets the left label at the given index.
     * 
     * @param index
     * @param obj
     * @throws IndexOutOfBoundsException if the given index is outside the
     * bounds of the left labels or if this InteractiveGrid was not
     * instantiated to have left labels.
     * @return 
     */
    public T setLeftLabel(int index, T obj) {
        if (this.LEFT_OFFSET == 0) {
            throw new UnsupportedOperationException("This grid was not "
                    + "instantiated to have labels on the left!");
        }
        return super.set(0, index, obj);
    }

    /**
     * Sets the right label at the given index.
     * 
     * @param index
     * @param obj
     * @throws IndexOutOfBoundsException if the given index is outside the
     * bounds of the right labels or if this InteractiveGrid was not
     * instantiated to have right labels.
     * @return 
     */
    public T setRightLabel(int index, T obj) {
        if (this.RIGHT_OFFSET == 0) {
            throw new UnsupportedOperationException("This grid was not "
                    + "instantiated to have labels on the right!");
        }
        T returnVal = super.set(this.getTotalColumns() - 1, index, obj);
        obj.setIcon(scaleIcon(obj.getIcon()));
        return returnVal;
    }
    // </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="Boolean methods">      
    public final boolean hasTopMargin() {
        return (MARGIN_TYPE & MARGIN_TOP) > 0;
    }

    public final boolean hasBottomMargin() {
        return (MARGIN_TYPE & MARGIN_BOTTOM) > 0;
    }

    public final boolean hasLeftMargin() {
        return (MARGIN_TYPE & MARGIN_LEFT) > 0;
    }

    public final boolean hasRightMargin() {
        return (MARGIN_TYPE & MARGIN_RIGHT) > 0;
    }

    /**
     * Returns true if this table has borders on the left of each cell.
     * @return 
     */
    public final boolean hasLeftBorder() {
        return (this.BORDER_TYPE & CELL_BORDER_LEFT) > 0;
    }

    /**
     * Returns true if this table has borders on the right of each cell.
     * @return 
     */
    public final boolean hasRightBorder() {
        return (this.BORDER_TYPE & CELL_BORDER_RIGHT) > 0;
    }

    /**
     * Returns true if this table has borders on top of each cell.
     * @return 
     */
    public final boolean hasTopBorder() {
        return (this.BORDER_TYPE & CELL_BORDER_TOP) > 0;
    }

    /**
     * Returns true if this table has borders on the bottom of each cell.
     * @return 
     */
    public final boolean hasBottomBorder() {
        return (this.BORDER_TYPE & CELL_BORDER_BOTTOM) > 0;
    }
    // </editor-fold>
}
